<?php

/**
 * Класс строка. Здесь под строкой подразумевается последовательность символов в некоторой кодировке.
 */
class str extends AbstractObject implements ArrayAccess, Iterator, Countable, SeekableIterator
{
    protected $_value = '';
    protected $_encoding = '';
    protected $_length = 0;
    protected $length_obsolete = false;

    static private $innerEncoding = 'UTF-8';

    /**
     * @param string/str $value
     * @param string $encoding
     */
    function __construct($value, $encoding = null)
    {
    	if (is_object($value) && is_a($value, __CLASS__)) {
    		$this->__copy($value);
    		return;
    	}

    	parent::__construct();
        $this->__readonly_fields[] = 'encoding';
        $this->__readonly_fields[] = 'length';
        if (!isset($encoding)) {
            $this->_encoding = Application::encoding();
        } else {
            if (! $this->allowEncoding($encoding)) {
                throw new ArgException();
            }
            $this->_encoding = $encoding;
        }

        $this->__set_value($value);
    }

    protected function __copy($obj)
    {
    	if (!(is_object($obj) && is_a($obj, __CLASS__))) {
    		throw new ArgException();
    	}
    	parent::__copy($obj);
        $this->_value          = $obj->_value;
        $this->_encoding       = $obj->_encoding;
        $this->_length         = $obj->_length;
        $this->length_obsolete = $obj->length_obsolete;
    }

    protected function __set_value($value)
    {
        if (! is_scalar($value)) {
            throw new ArgException();
        }
        $this->_value = strval($value);
        $this->length_obsolete = true;

        return $this->value;
    }

    static function allowEncoding($value)
    {
    	$value = new self($value);
        if (!in_array($value->toString(), mb_list_encodings())) {
            return false;
        }
        return true;
    }

    static function normalizeEncoding($value)
    {
    	$value = new self($value);
        if (! self::allowEncoding($value)) {
            throw new ArgException();
        }
        return $value->toString();
    }

    protected function __get_length()
    {
        if ($this->length_obsolete) {
            $this->_length = mb_strlen($this->value, $this->encoding);
        }
        return $this->_length;
    }

    /**
     * Меняет кодировку.
     *
     * @param string $new_encoding
     * @return istr
     */
    function convertTo($new_encoding = null)
    {
        if ($new_encoding === $this->encoding) {
            return $this;
        }
        $new_encoding = $this->normalizeEncoding($new_encoding);

        $this->_value = mb_convert_encoding($this->_value, $new_encoding, $this->encoding);
        $this->length_obsolete = true;
        $this->_encoding = $new_encoding;

        return $this;
    }

    function __toString()
    {
        return $this->toString();
    }

    /**
     * Возвращает содержимое в Application::encoding().
     *
     * @return string
     */
    function toString()
    {
        $obj = clone $this;
        $obj->convertTo(Application::encoding());
        return $obj->value;
    }

    /**
     * Возвращает символ по его номеру (с нуля).
     *
     * @param int $index
     * @return string
     */
    function charAt($index)
    {
        return $this->substr($index, 1)->value;
    }

    /**
     * Возвращает символ в Application::encoding().
     *
     * @param int $index
     * @return string
     */
    function toChar($index)
    {
        return str($this->charAt($index), $this->encoding)->convertTo(Application::encoding())->value;
    }

    /**
     * Вставляет строку в конец.
     *
     * @param str/string $str
     * @return istr
     */
    function append($str)
    {
        $tail = new self($str);
        $this->__set_value(self::concat($this, $tail, $this->encoding)->value);

        return $this;
    }

    /**
     * Вставляет строку в начало.
     *
     * @param str/string $str
     * @return istr
     */
    function insertBefore($str)
    {
        $head = new self($str);
        $this->__set_value(self::concat($head, $this, $this->encoding)->value);

        return $this;
    }

    /**
     * Конкатенация строк.
     *
     * @param str/string $head
     * @param str/string $tail
     * @param string $output_encoding
     * @return istr
     */
    static function concat($head, $tail, $output_encoding = null)
    {
        $head = new self($head->value, $output_encoding);
        $tail = new self($tail->value, $output_encoding);

        $new_value = $head->value . $tail->value;

        return new self($new_value, $output_encoding);
    }

    /**
     * Возвращает фрагмент строки
     *
     * @param int $start
     * @param int $length
     * @return istr
     */
    function substr($start, $length = null)
    {
        $start = $this->normalizeIndex($start);
        if (! $this->isExistsIndex($start)) {
            throw new ArgException();
        }
        if (! is_null($length)) {
            $length = $this->normalizeInt($length);
        } else {
            $length = $this->length - $start;
        }
        return new self(mb_substr($this->value, $start, $length, $this->encoding), $this->encoding);
    }

    /**
     * Преобразует в строчные.
     *
     * @return istr
     */
    function toLowerCase()
    {
        $this->__set_value(mb_strtolower($this->value, $this->encoding));
        return $this;
    }

    /**
     * Преобразует в прописные.
     *
     * @return istr
     */
    function toUpperCase()
    {
        $this->__set_value(mb_strtoupper($this->value, $this->encoding));
        return $this;
    }

    /**
     * Режет строку в массив по разделителю.
     *
     * @param string/str $delimiter Разделитель
     * @param int[optional] $limit Максимальное кол-во частей.
     * @return array of str
     */
    function split($delimiter, $limit = null)
    {
    	return $this->_eregSplit(false, $delimiter, $limit);
    }

    /**
     * Режет строку в массив по regex-шаблону.
     *
     * @param string/str $pattern Шаблон
     * @param int[optional] $limit Максимальное кол-во частей.
     * @return array of str
     */
    function eregSplit($pattern, $limit = null)
    {
    	return $this->_eregSplit(true, $pattern, $limit);
    }

    /**
     * Режет строку в массив по regex-шаблону/разделителю.
     *
     * @access protected
     * @param bool $ereg Если true, то $delimiter воспринимается как regex-шаблон, иначе — как разделитель.
     * @param string/str $delimiter Шаблон/разделитель
     * @param int[optional] $limit Максимальное кол-во частей.
     * @return array of str
     */
    protected function _eregSplit($ereg, $delimiter, $limit = null)
    {
    	if (isset($limit) && (!is_int($limit) || $limit < 0)) {
    		throw new ArgException();
    	}
    	if ($limit === 0) {
    		return array();
    	}
    	$delimiter = new self($delimiter);
    	try {
    		$delimiter->convertTo($this->encoding);
    	} catch (Exception $e) {
    		return array(clone $this);
    	}
    	if (! $ereg) {
	    	$delimiter = self::ereg_escape($delimiter);
    	}

    	$old_encoding = mb_regex_encoding();
    	mb_regex_encoding($this->encoding);
    	if ($limit) {
    	   $tmp_result = mb_split($delimiter->value, $this->value, $limit);
    	} else {
    	   $tmp_result = mb_split($delimiter->value, $this->value);
    	}
    	mb_regex_encoding($old_encoding);

    	$result = array();
    	foreach ($tmp_result as $str) {
    		$result[] = new self($str, $this->encoding);
    	}

    	return $result;
    }

    /**
     * Проверяет строку на соответствие regex-шаблону.
     *
     * @param string/str $pattern Шаблон
     * @return bool
     */
    function ereg($pattern)
    {
    	return $this->_ereg(false, $pattern);
    }

    /**
     * Нечувствительный к регистру аналог ereg().
     *
     * @param string/str $pattern Шаблон
     * @return bool
     */
    function eregi($pattern)
    {
    	return $this->_ereg(true, $pattern);
    }

    /**
     * Проверяет, содержит ли строка подстроку $substr.
     *
     * @param string/str $substr Подстрока
     * @return bool
     */
    function contains($substr)
    {
        return ($this->indexOf($substr) >= 0);
    }

    /**
     * Проверяет, начинается ли строка с подстроки $substr.
     *
     * @param string/str $substr Подстрока
     * @return bool
     */
    function startsWith($substr)
    {
        return ($this->indexOf($substr) == 0);
    }

    /**
     * Возвращает начальную позицию первого вложения подстроки $substr. Если вложения нет, — возвращает -1.
     *
     * @param string/str $substr
     * @param int[optional] $offset Смещение к концу относительно начала.
     * @return int
     */
    function indexOf($substr, $offset = null)
    {
    	$substr = new self($substr);
    	try {
            $substr->convertTo($this->encoding);
    	} catch (Exception $e) {
    	    return -1;
    	}
    	if (isset($offset)) {
    	    if (!is_int($offset) || $offset < 0) {
        		throw new ArgException();
    	    }
        	if (($this->length - $offset) < $substr->length) {
        		return -1;
        	}
    	}
    	$result = mb_strpos($this->value, $substr->value, $offset, $this->encoding);
    	if ($result === false) {
            $result = -1;
    	}
        return intval($result);
    }

    /**
     * Проверяет строку на соответствие regex-шаблону.
     *
     * @access protected
     * @param bool $case_insensetive Если true, то нечувствительная к регистру.
     * @param string/str $pattern Шаблон
     * @return bool
     */
    protected function _ereg($case_insensetive, $pattern)
    {
    	$pattern = new self($pattern);
    	try {
    		$pattern->convertTo($this->encoding);
    	} catch (Exception $e) {
    		return false;
    	}

    	$old_encoding = mb_regex_encoding();
    	mb_regex_encoding($this->encoding);
    	if ($case_insensetive) {
    	    $ppp = $pattern->replace("/", "\\/")->convertTo("UTF-8")->insertBefore("/")->append("/iu")->value;
    	    $subject = str($this)->convertTo("UTF-8")->value;
    		$result = preg_match($ppp, $subject);
    	} else {
    		$result = mb_ereg($pattern->value, $this->value);
    	}
    	mb_regex_encoding($old_encoding);

    	return (bool)$result;
    }

    /**
     * Заменяет все фрагменты строки, соответствующие regex-шаблону $pattern строкой $replacement.
     *
     * @param string/str $pattern Regex-шаблон
     * @param string/str $replacement Подстановка
     * @return istr
     */
    function eregReplace($pattern, $replacement)
    {
    	return $this->_eregReplace(false, $pattern, $replacement);
    }

    /**
     * Тоже что и eregReplace(), только нечувствительная к регистру.
     *
     * @param string/str $pattern Regex-шаблон
     * @param string/str $replacement Подстановка
     * @return istr
     */
    function eregiReplace($pattern, $replacement)
    {
    	return $this->_eregReplace(true, $pattern, $replacement);
    }

    /**
     * Заменяет все фрагменты строки, соответствующие regex-шаблону $pattern строкой $replacement.
     *
     * @access protected
     * @param bool $case_insensetive Если true, то нечувствительная к регистру.
     * @param string/str $pattern Regex-шаблон
     * @param string/str $replacement Подстановка
     * @return istr
     */
    protected function _eregReplace($case_insensetive, $pattern, $replacement)
    {
    	$pattern = new self($pattern);
    	$replacement = new self($replacement);
    	try {
    		$pattern->convertTo($this->encoding);
    	} catch (Exception $e) {
    		return $this;
    	}
    	try {
    		$replacement->convertTo($this->encoding);
    	} catch (Exception $e) {
    		throw new Exception();
    	}

    	$old_encoding = mb_regex_encoding();
    	mb_regex_encoding($this->encoding);
    	$tmp_result = "";
    	if ($case_insensetive) {
    	    $ppp = $pattern->replace("/", "\\/")->convertTo("UTF-8")->insertBefore("/")->append("/iu")->value;
    	    $subject = str($this)->convertTo("UTF-8")->value;
    		$tmp_result = preg_replace($ppp, $replacement->convertTo("UTF-8")->value, $subject);
    		$tmp_result = str($tmp_result, "UTF-8")->convertTo($this->encoding)->value;
    	} else {
	    	$tmp_result = mb_ereg_replace($pattern->value, $replacement->value, $this->value);
    	}
    	mb_regex_encoding($old_encoding);

    	$this->__set_value($tmp_result);

    	return $this;
    }

    /**
     * Удаление пробельных символов в начале строки.
     *
     * @return istr
     */
    function lTrim()
    {
    	return $this->eregReplace("^\s+", "");
    }

    /**
     * Удаление пробельных символов в конце строки.
     *
     * @return istr
     */
    function rTrim()
    {
    	return $this->eregReplace("\s+$", "");
    }

    /**
     * Удаление пробельных символов по краям строки.
     *
     * @return istr
     */
    function trim()
    {
    	return $this->lTrim()->rTrim();
    }

    /**
     * Удаление пробельных символов в начале и в конце строки.
     * Так же слитие пробельных последовательностей внутри строки в один пробел.
     *
     * @return istr
     */
    function normalizeSpace()
    {
    	return $this->trim()->eregReplace("\s+", " ");
    }

    /**
     * Экранирование последовательности символов для вставки в regex.
     *
     * @return istr
     */
    static function ereg_escape($expr)
    {
		$expr = new self($expr);
    	$chars = array(
    		"\\", "+", "*", "?", "[", "^", "]", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":",
		);
		$result = new self("", $expr->encoding);
        for ($i = 0; $i < $expr->length; $i++) {
            if (in_array($expr->toChar($i), $chars, true)) {
                $result->append(new self("\\"));
                $result->append($expr->charAt($i));
            } else {
                $result->append($expr->charAt($i));
            }
        }

		return $result;
    }

    /**
     * Разбивает текст на массив строк по признаку конца строки.
     *
     * @param string/str $data
     * @param string/str $EOL Признак конца строки (можно только "\r\n", "\r" и "\n").
     * @return array of string
     */
    function splitText($EOL = null)
    {
        if (isset($EOL)) {
            $EOL = str($EOL)->toString();
            if (! in_array($EOL, array("\r\n", "\r", "\n"), true)) {
        		throw new ArgException();
            }
        }
    	$tmp_lines = $this->split(isset($EOL) ? $EOL : "\r\n");
    	if (isset($EOL)) {
    	    $lines = $tmp_lines;
    	} else {
    	    $lines = array();
    	    foreach ($tmp_lines as $tmp_line) {
    	        foreach ($tmp_line->split("\r") as $tl) {
                    $lines = ar::concat($lines, $tl->split("\n"));
    	        }
    	    }
    	}

        return $lines;
    }

    /**
     * Заменяет все подстроки $searche подстроками $replacement.
     *
     * @param string/istr $searche
     * @param string/istr $replacement
     * @return istr
     */
    function replace($searche, $replacement)
    {
    	$searche = new self($searche);
    	$replacement = new self($replacement);
    	try {
            $searche->convertTo($this->encoding);
    	} catch (Exception $e) {
    	    return $this;
    	}
    	try {
            $replacement->convertTo($this->encoding);
    	} catch (Exception $e) {
    		throw new ArgException();
    	}
    	$searche = self::ereg_escape($searche);
        return $this->eregReplace($searche, $replacement);
    }

    /**
     * Преобразует символы в соответствующие XML-сущности.
     *
     * @param int $quote_style
     * ENT_COMPAT Преобразуются двойные кавычки, одиночные остаются без изменений.
     * ENT_QUOTES Преобразуются и двойные, и одиночные кавычки.
     * ENT_NOQUOTES И двойные, и одиночные кавычки остаются без изменений.
     *
     * @return istr
     */
    function xmlEntities($quote_style = ENT_QUOTES)
    {
    	$this->replace("&", "&amp;")->replace("<", "&lt;")->replace(">", "&gt;");
    	if (($quote_style == ENT_QUOTES) || ($quote_style == ENT_COMPAT)) {
    	    $this->replace('"', "&quot;");
    	} elseif ($quote_style == ENT_QUOTES) {
    	    $this->replace("'", "&apos;");
    	}

    	return $this;
      }

    /**
     * Заменяет все управляющие символы (0—31) экранированными последовательностями \NN.
     *
     * @param string/istr $addition_chars Строка с дополнительно экранируемыми символами (слеш будет экранирован всегда).
     * @return istr
     */
    function addslashes($addition_chars = "")
    {
    	$addition_chars = new self($addition_chars);
    	$addition_chars_array = array();
        for ($i = 0; $i < $addition_chars->length; $i++) {
            $addition_chars_array[] = $addition_chars->toChar($i);
        }
        $addition_chars_array[] = "\\";
    	$result = new self("");
        for ($i = 0; $i < $this->length; $i++) {
            $char = $this->toChar($i);
            if (in_array($char, $addition_chars_array)) {
                $result->append(new self("\\"));
                $result->append($char);
            } elseif ((strlen($char) == 1) && (ord($char) < 32)) {
                $code = "";
                switch (ord($char)) {

                case 0x07:
                    $code = "a";
                    break;

                case 0x08:
                    $code = "b";
                    break;

                case 0x09:
                    $code = "t";
                    break;

                case 0x0A:
                    $code = "n";
                    break;

                case 0x0B:
                    $code = "v";
                    break;

                case 0x0C:
                    $code = "f";
                    break;

                case 0x0D:
                    $code = "r";
                    break;

                default:
                    $code = strval(intval(ord($char), 8));
                }
                $result->append(new self("\\"));
                $result->append($code);
            } else {
                $result->append($char);
            }
        }
        $this->__set_value($result->value);

        return $this;
    }

    /**
     * Возвращает код символа в системе unicode по его номеру (с нуля).
     *
     * @param int $index
     * @return int
     */
    function charCodeAt($index)
    {
        $o = mb_convert_encoding($this->charAt($index), 'byte4be', $this->encoding);
        return (ord($o[0]) << 24) + (ord($o[1]) << 16) + (ord($o[2]) << 8) + ord($o[3]);
    }

    /**
     * Возвращает код символа в системе Unicode.
     *
     * @param int $index
     * @return int
     */
    static function getCode($char)
    {
        return str($char)->charCodeAt(0);
    }

    /**
     * Возвращает символ по его коду Unicode.
     *
     * @param int $code
     * @return string
     */
    static function getChar($code)
    {
        if (! is_numeric($code)) {
            throw new ArgException();
        }
        $code = intval($code);
        if ($code < 0) {
            throw new ArgException();
        }
        $octets = chr(($code >> 24) % 256).chr(($code >> 16) % 256).chr(($code >> 8) % 256).chr($code % 256);
        return new self(mb_convert_encoding($octets, self::$innerEncoding, 'byte4be'), self::$innerEncoding);
    }

    /**
     * Создает строку из символов, заданных кодами Unicode.
     *
     * В JavaScrip’е этот метод принимает только двухбайтные коды
     * (если больше — берётся остаток от деления на 65536).
     * Так же как и эскейп-последовательности в строковом литерале \uHHHH.
     *
     * @param int $code1, $code2,… $codeN
     * @return string
     */
    static function fromCharCode()
    {
        $result = new self("", self::$innerEncoding);
        $args =& func_get_args();
        foreach ($args as $code) {
            $result->append(self::getChar($code));
        }
        return $result;
    }

    /**
     * Соединяет массив строк в одну с использованием разделителя.
     *
     * @param string $glue
     * @param array $pieces
     * @return int
     */
    static function implode($glue, $pieces)
    {
        if (! ar::isArray($pieces)) {
            throw new ArgException();
        }
        $glue = new self($glue);

        $result = new self("");

        $count = 0;
        foreach ($pieces as $v) {
            if ($count++) {
                $result->append($glue);
            }
            $result->append($v);
        }

        return $result;
    }

    /**
     * Возвращает строку из двоичных данных (октетов).
     *
     * @param string $stream
     * @return istr
     */
    static function fromOctets($stream)
    {
        return new self($stream, 'ISO-8859-1');
    }

    /**
     * Нормализует целочисленное значение.
     *
     * @param mixed $index
     * @return int
     */
    protected function normalizeInt($value)
    {
        if (! is_int($value)) {
            if (! is_numeric($value)) {
                throw new ArgException();
            } else {
                $value = intval($value);
            }
        }

        return $value;
    }

    /**
     * Нормализует индекс.
     *
     * @param mixed $index
     * @return int
     */
    protected function normalizeIndex($index)
    {
        return $this->normalizeInt($index);
    }

    /**
     * Проверяет существует ли указанный индекс.
     *
     * @param int $index
     * @return bool
     */
    protected function isExistsIndex($index)
    {
        return (($index < $this->length) && ($index >= 0));
    }

    /**
     * Реализация интерфейса прямой индексации ArrayAccess.
     */

    /**
     * Проверяет существует ли указанное смещение.
     *
     * @param mixed $index
     * @return bool
     */
    public function offsetExists($index)
    {
        $index = $this->normalizeIndex($index);
        return $this->isExistsIndex($index);
    }

    /**
     * Возврат значения по указанному смещению.
     *
     * @param mixed $index
     * @return string
     */
    public function offsetGet($index)
    {
        return $this->charAt($index);
    }

    /**
     * Установка символа в текущую позицию.
     *
     * @param mixed $index
     * @param string/str $value
     * @return string
     */
    public function offsetSet($index, $value)
    {
        $value = str($value);
        if (! $value->length) {
            throw new ArgException();
        }
        if (isset($index)) {
            $index = $this->normalizeIndex($index);
            if (! $this->isExistsIndex($index)) {
                throw new ArgException();
            }
            $before = $this->substr(0, $index);
            $before->append($value->substr(0, 1));
            if ($this->isExistsIndex($index + 1)) {
                $before->append($this->substr($index + 1));
            }
            $this->__set_value($before->value);
        } else {
            $this->append($value->substr(0, 1));
        }
    }

    /**
     * Заглушка для удаления символа из строки.
     *
     * @param mixed $index
     */
    public function offsetUnset($index)
    {
        throw new ArchException();
    }

    /**
     * Реализация интерфейса Iterator.
     */

    /**
     * Текущая позиция итератора.
     *
     * @var int
     */
    protected $iterator_index = 0;

    public function valid()
    {
        return $this->isExistsIndex($this->iterator_index);
    }

    public function current()
    {
        return $this->charAt($this->iterator_index);
    }

    public function key()
    {
        return $this->iterator_index;
    }

    public function next()
    {
        $this->iterator_index++;
    }

    public function rewind()
    {
        return $this->charAt($this->iterator_index = 0);
    }

    /**
     * Реализация интерфейса Countable.
     */

    /**
     * Возвращает кол-во символов в строке.
     *
     * @return int
     */
    public function count()
    {
        return $this->length;
    }

    /**
     * Реализация интерфейса SeekableIterator.
     */

    /**
     * Переводит текущюю позицию итератора в значение $position.
     *
     * @param int $position
     */
    public function seek($position)
    {
        $position = $this->normalizeIndex($position);
        $this->iterator_index = $position;
    }
}

/*abstract*/ class istr extends str
{
    /**
     * Кодировка, в которой находится содержимое value.
     *
     * @var string
     */
    public $encoding;

    /**
     * Содержимое, закодираванное в кодировке encoding.
     *
     * @var string
     */
    public $value;

    /**
     * Длинна строки в символах.
     *
     * @var int
     */
    public $length;
}

/**
 * Создаёт/копирует объект str
 *
 * @param string/str $value
 * @param string[optional] $encoding
 * @return istr
 */
function &str($value, $encoding = null)
{
    $args = func_get_args();
    return ____createO(__FUNCTION__, $args);
}

?>